#include "PGL.h"

#include <gl/glew.h>

#include <fstream>
#include <sstream>
#include <algorithm>
#include <string>
using namespace std;

PGL::CLGL::CLGL():
	mError(PGL_SUCCESS),
	clmPixelBuffer(NULL),
	pchPixelBuffer(NULL)
{
	// Initialize OpenCL 
	mOCLError = initOCL();
	if(mOCLError != CL_SUCCESS)
		mError = PGL_OPENCL_ERROR;

	// Initialize default kernels
	mOCLError = initDefaultKernels();
	if(mOCLError != CL_SUCCESS)
		mError = PGL_OPENCL_ERROR;

	// Initialize lookup maps
	initMapErrorEnumToErrorString();
}

// *********************************************** //
// Initialize OpenCL
// *********************************************** //
cl_int PGL::CLGL::initOCL()
{
	// Platform, CPU+GPU devices, context, and command queues
	mOCLError = initPlatform(string("Intel(R) Corporation"));
	CHECKANDRET(mOCLError);

	mOCLError = initDevice(CL_DEVICE_TYPE_CPU);
	CHECKANDRET(mOCLError);

	mOCLError = initDevice(CL_DEVICE_TYPE_GPU);
	CHECKANDRET(mOCLError);

	mOCLError = initContext();
	CHECKANDRET(mOCLError);

	mOCLError = initCommandQueues();
	CHECKANDRET(mOCLError);

	return mOCLError;
}

cl_int PGL::CLGL::initPlatform(string vendor)
{
	// Get OCL platform list
	cl_uint numPlatforms;
	mOCLError = clGetPlatformIDs(0,NULL,&numPlatforms);
	CHECKANDRET(mOCLError);

	cl_platform_id *platformIds = new cl_platform_id[numPlatforms];
	mOCLError = clGetPlatformIDs(numPlatforms,platformIds,NULL);
	CHECKANDRET(mOCLError);

	// Find platform that matches input vendor string
	for(cl_uint i=0; i<numPlatforms; i++)
	{
		size_t vendorCharArrSize;
		mOCLError = clGetPlatformInfo(platformIds[i],CL_PLATFORM_VENDOR,0,NULL,&vendorCharArrSize);
		CHECKANDRET(mOCLError);

		char *vendorCharArr = new char[vendorCharArrSize];
		mOCLError = clGetPlatformInfo(platformIds[i],CL_PLATFORM_VENDOR,vendorCharArrSize,vendorCharArr,NULL);
		CHECKANDRET(mOCLError);

		string vendorString(vendorCharArr);
		
		if(vendorString == vendor)
		{
			mPlatformID = platformIds[i];
			return CL_SUCCESS;
		}
	}

	// Platform not found!
	return CL_INVALID_PLATFORM;
}

cl_int PGL::CLGL::initDevice(cl_device_type devtype)
{
	// Choose which pointer to populate (CPU or GPU)
	cl_device_id *devId = (devtype == CL_DEVICE_TYPE_GPU) ? &mGPUDeviceID : &mCPUDeviceID;

	// Get device list
	cl_uint numDevices;
	mOCLError = clGetDeviceIDs(mPlatformID,devtype,0,NULL,&numDevices);
	CHECKANDRET(mOCLError);

	if(numDevices>0)
	{
		mOCLError = clGetDeviceIDs(mPlatformID,devtype,1,devId,NULL);
		CHECKANDRET(mOCLError);
	}
	else
	{
		// No devices found for given type!
		return CL_DEVICE_NOT_FOUND;
	}

	return mOCLError;
}

cl_int PGL::CLGL::initContext()
{
	cl_context_properties ctxProps[] = 
	{
		CL_CONTEXT_PLATFORM,
		(cl_context_properties) mPlatformID,
		0
	};

	// Create context for this platform (must be for platform, not device!)
	cl_device_id cpuAndGpu[] = { mCPUDeviceID, mGPUDeviceID };
	mContext = clCreateContext(ctxProps,2,cpuAndGpu,NULL,NULL,&mOCLError);
	CHECKANDRET(mOCLError);

	return mOCLError;
}

cl_int PGL::CLGL::initCommandQueues()
{
	// Create command queues for CPU and GPU
	mCPUCommandQueue = clCreateCommandQueue(mContext,mCPUDeviceID,CL_QUEUE_PROFILING_ENABLE,&mOCLError);
	CHECKANDRET(mOCLError);

	mGPUCommandQueue = clCreateCommandQueue(mContext,mGPUDeviceID,CL_QUEUE_PROFILING_ENABLE,&mOCLError);
	CHECKANDRET(mOCLError);

	return mOCLError;
}

// *********************************************** //
// Initialize default kernels
// *********************************************** //

cl_int PGL::CLGL::initDefaultKernels()
{
	mOCLError = initDefaultPixelStates();
	CHECKANDRET(mOCLError);

	mOCLError = initDefaultVertexStates();
	CHECKANDRET(mOCLError);

	return mOCLError;
}

cl_int PGL::CLGL::initDefaultPixelStates()
{
	vector<cl_kernel> pixelKernels;
	string pixelKernelsFilename("..\\PGL\\PixelKernels.cl");
	vector<string> pixelKernelNames;
	pixelKernelNames.push_back("fillColor");

	createProgramAndKernels(pixelKernelsFilename,pixelKernelNames,&clpPixelProgram,pixelKernels);

	clearColorState.kernel = pixelKernels[0];

	argument pixelArgument,widthArgument,heightArgument,colorArgument;
	pixelArgument.argsize = sizeof(cl_mem);
	widthArgument.argsize = sizeof(cl_uint);
	heightArgument.argsize = sizeof(cl_uint);
	colorArgument.argsize = sizeof(cl_uchar4);

	clearColorState.arguments.push_back(pixelArgument);
	clearColorState.arguments.push_back(widthArgument);
	clearColorState.arguments.push_back(heightArgument);
	clearColorState.arguments.push_back(colorArgument);

	clearColorState.argumentNames["pixels"] = 0;
	clearColorState.argumentNames["width"] = 1;
	clearColorState.argumentNames["height"] = 2;
	clearColorState.argumentNames["color"] = 3;

	return mOCLError;
}

cl_int PGL::CLGL::initDefaultVertexStates()
{
	vector<cl_kernel> vertexKernels;
	string vertexKernelsFilename("..\\PGL\\VertexKernels.cl");
	vector<string> vertexKernelNames;
	vertexKernelNames.push_back("vertexTransform");
	vertexKernelNames.push_back("rasterizer");

	createProgramAndKernels(vertexKernelsFilename,vertexKernelNames,&clpVertexProgram,vertexKernels);

	// Extract vertex transform kernel and map arguments
	vertexTransformState.kernel = vertexKernels[0];

	argument vertexArg, outVertexArg, matrixArg, matrixNumArg, 
		viewportArg, pixelArg;
	vertexArg.argsize = sizeof(cl_mem);
	outVertexArg.argsize = sizeof(cl_mem);
	matrixArg.argsize = sizeof(cl_mem);
	matrixNumArg.argsize = sizeof(cl_uint);
	viewportArg.argsize = sizeof(cl_uint4);
	pixelArg.argsize = sizeof(cl_mem);

	vertexTransformState.arguments.push_back(vertexArg);
	vertexTransformState.arguments.push_back(outVertexArg);
	vertexTransformState.arguments.push_back(matrixArg);
	vertexTransformState.arguments.push_back(matrixNumArg);
	vertexTransformState.arguments.push_back(viewportArg);
	vertexTransformState.arguments.push_back(pixelArg);

	vertexTransformState.argumentNames["vertices"] = 0;
	vertexTransformState.argumentNames["outVertices"] = 1;
	vertexTransformState.argumentNames["matrices"] = 2;
	vertexTransformState.argumentNames["numMatrices"] = 3;
	vertexTransformState.argumentNames["viewport"] = 4;
	vertexTransformState.argumentNames["pixels"] = 5;

	// Extract rasterizer kernel and map arguments
	rasterState.kernel = vertexKernels[1];

	argument widthArg, heightArg;
	widthArg.argsize = sizeof(cl_uint);
	heightArg.argsize = sizeof(cl_uint);

	rasterState.arguments.push_back(pixelArg);
	rasterState.arguments.push_back(widthArg);
	rasterState.arguments.push_back(heightArg);
	rasterState.arguments.push_back(vertexArg);

	rasterState.argumentNames["pixels"] = 0;
	rasterState.argumentNames["width"] = 1;
	rasterState.argumentNames["height"] = 2;
	rasterState.argumentNames["vertices"] = 3;

	return mOCLError;
}


cl_int PGL::CLGL::createProgramAndKernels(
	string fileName, vector<string> kernelNames, 
	cl_program *programOut, vector<cl_kernel> &kernelsOut)
{
	string programString = readFileToString(fileName);

	const char *charArrArr[] = {programString.c_str()};

	cl_device_id bothDevices[] = {mCPUDeviceID,mGPUDeviceID};

	*programOut = clCreateProgramWithSource(
		mContext,
		1,
		charArrArr,
		NULL,
		&mOCLError);
	CHECKANDRET(mOCLError);

	mOCLError = clBuildProgram(
		*programOut,
		2,
		bothDevices,
		programString.c_str(),
		NULL,
		NULL);
	CHECKANDRET(mOCLError);

	for(unsigned int i=0; i<kernelNames.size(); i++)
	{
		cl_kernel tmpKernel = clCreateKernel(*programOut,kernelNames[i].c_str(),&mOCLError);
		CHECKANDRET(mOCLError);

		kernelsOut.push_back(tmpKernel);
	}

	return mOCLError;
}

std::string PGL::CLGL::readFileToString( string fileName )
{
	ifstream fileStream;
	string fileString;
	char tmpChar;

	// Check input kernel file
	fileStream.open(fileName);
	if(!fileStream.is_open())
	{
		mError = PGL_FILE_NOT_FOUND;
	}

	// Read kernel file into stringstream
	while(fileStream.get(tmpChar))
	{
		fileString.push_back(tmpChar);
	}

	return fileString;
}

// *********************************************** //
// Initialize lookup maps
// *********************************************** //

void PGL::CLGL::initMapErrorEnumToErrorString()
{
	mapErrorEnumToErrorString[PGL_SUCCESS] = "Success";
	mapErrorEnumToErrorString[PGL_OPENCL_ERROR] = "OpenCL Error (check getOpenCLErrorAsInt() or getOpenCLErrorAsString())";
	mapErrorEnumToErrorString[PGL_NO_SUCH_LIGHT] = "No such light (you tried to remove a light that hasn't been added to this scene)";
}

// *********************************************** //
// Main functions
// *********************************************** //

void PGL::CLGL::resize(cl_uint width, cl_uint height)
{
	gfxWidth = width;
	gfxHeight = height;

	if(pchPixelBuffer)
	{
		mOCLError = clEnqueueUnmapMemObject(
			mCPUCommandQueue,
			clmPixelBuffer,
			pchPixelBuffer,
			0,
			NULL,
			NULL);
	}

	if(clmPixelBuffer)
		clReleaseMemObject(clmPixelBuffer);

	clmPixelBuffer = clCreateBuffer(
		mContext,
		CL_MEM_READ_WRITE,
		sizeof(cl_uchar4)*gfxWidth*gfxHeight,
		NULL,
		&mOCLError);

	if(mOCLError)
	{
		mError = PGL_OPENCL_ERROR;
		return;
	}

	pchPixelBuffer = (cl_uchar4*)clEnqueueMapBuffer(
		mGPUCommandQueue,
		clmPixelBuffer,
		CL_TRUE,
		CL_MAP_READ | CL_MAP_WRITE,
		0,
		sizeof(cl_uchar4)*gfxWidth*gfxHeight,
		0,
		NULL,
		NULL,
		&mOCLError);

	if(mOCLError)
		mError = PGL_OPENCL_ERROR;
}

void PGL::CLGL::setViewport( cl_uint x, cl_uint y, cl_uint width, cl_uint height )
{
	gfxViewport.s[0] = x;
	gfxViewport.s[1] = y;
	gfxViewport.s[2] = width;
	gfxViewport.s[3] = height;
}

// *********************************************** //
// Buffer Operations
// *********************************************** //
void PGL::CLGL::setClearColor( cl_uchar4 clearColor )
{
	gfxClearColor = clearColor;
	clearColorState.arguments[clearColorState.argumentNames["color"]].argptr = (void*)&gfxClearColor;
}

void PGL::CLGL::setDefaultPipelineArgs()
{
	// Set args for clear color state
	clearColorState.arguments[clearColorState.argumentNames["pixels"]].argptr = (void*)&clmPixelBuffer;
	clearColorState.arguments[clearColorState.argumentNames["width"]].argptr = (void*)&gfxWidth;
	clearColorState.arguments[clearColorState.argumentNames["height"]].argptr = (void*)&gfxHeight;
	clearColorState.arguments[clearColorState.argumentNames["color"]].argptr = (void*)&gfxClearColor;

	clearColorState.dimensions = 2;
	clearColorState.globalWorkSize = new cl_uint[2];
	clearColorState.globalWorkSize[0] = gfxWidth;
	clearColorState.globalWorkSize[1] = gfxHeight;
	clearColorState.localWorkSize = NULL;

	// Set args for vertex transform state
	vertexTransformState.arguments[vertexTransformState.argumentNames["viewport"]].argptr = (void*)&gfxViewport;
	vertexTransformState.arguments[vertexTransformState.argumentNames["pixels"]].argptr = (void*)&clmPixelBuffer;

	// Set args for rasterizer state
	rasterState.arguments[rasterState.argumentNames["width"]].argptr = (void*)&gfxWidth;
	rasterState.arguments[rasterState.argumentNames["height"]].argptr = (void*)&gfxHeight;
	rasterState.arguments[rasterState.argumentNames["pixels"]].argptr = (void*)&clmPixelBuffer;
}

void PGL::CLGL::unmapPixelBuffer( cl_command_queue cmdQueue, vector<cl_event> &eventVector )
{
	// Unmap
	cl_event unmapEvent;
	mOCLError = clEnqueueUnmapMemObject(
		cmdQueue,
		clmPixelBuffer,
		pchPixelBuffer,
		eventVector.size(),
		eventVector.size() ? &eventVector[0] : NULL,
		&unmapEvent);

	if(mOCLError != CL_SUCCESS)
		mError = PGL_OPENCL_ERROR;

	eventVector.push_back(unmapEvent);
}

void PGL::CLGL::remapPixelBuffer( cl_command_queue cmdQueue, vector<cl_event> &eventVector )
{
	// Remap
	cl_event remapEvent;
	pchPixelBuffer = (cl_uchar4*)clEnqueueMapBuffer(
		cmdQueue,
		clmPixelBuffer,
		CL_TRUE,
		CL_MAP_READ | CL_MAP_WRITE,
		0,
		sizeof(cl_uchar4)*gfxWidth*gfxHeight,
		0,//eventVector.size(),
		NULL,//eventVector.size() ? &eventVector[0] : NULL,
		NULL,//&remapEvent,
		&mOCLError);

	if(mOCLError != CL_SUCCESS)
		mError = PGL_OPENCL_ERROR;

	//eventVector.push_back(remapEvent);
}

void PGL::CLGL::runPipeline()
{
	enqueuePipeline();
	finishPipeline();
}

void PGL::CLGL::enqueuePipeline()
{
	clFinish(mGPUCommandQueue);
	clFinish(mCPUCommandQueue);

	unmapPixelBuffer(mGPUCommandQueue,mPipelineEvents);

	// Clear pixel buffer
	mOCLError = clearColorState.enqueue(mGPUCommandQueue,mPipelineEvents);

	// Run vertex transform
	mOCLError = vertexTransformState.enqueue(mGPUCommandQueue,mPipelineEvents);

	// Run vertex transform
	mOCLError = rasterState.enqueue(mGPUCommandQueue,mPipelineEvents);

	remapPixelBuffer(mGPUCommandQueue,mPipelineEvents);
}

void PGL::CLGL::finishPipeline()
{
	mOCLError = clWaitForEvents(mPipelineEvents.size(),&mPipelineEvents[0]);

	for (unsigned int i=0; i<mPipelineEvents.size(); i++)
	{
		clReleaseEvent(mPipelineEvents[i]);
	}

	mPipelineEvents.clear();
}

void PGL::CLGL::clearDepthBuffer()
{

}

void PGL::CLGL::drawPixelsToGl()
{
	// Draw pixels to gl buffer
	glDrawPixels(gfxWidth,gfxHeight,GL_RGBA,GL_UNSIGNED_BYTE,pchPixelBuffer);
}


// *********************************************** //
// Error lookups
// *********************************************** //
PGL::PGLErrorEnum PGL::CLGL::getCurrentErrorAsEnum()
{
	return mError;
}

std::string PGL::CLGL::getCurrentErrorAsString()
{
	return mapErrorEnumToErrorString[mError];
}

PGL::pglMemObject PGL::CLGL::createMemObject( void *input, size_t memSize )
{
	pglMemObject result;

	result.memSize = memSize;

	// Create actual mem object
	result.memObject = clCreateBuffer(
		mContext,
		CL_MEM_READ_WRITE | (input ? CL_MEM_COPY_HOST_PTR : 0),
		memSize,
		input,
		&mOCLError);
	if(mOCLError)
	{
		mError = PGL_OPENCL_ERROR;
		return result;
	}

	// Remap new vertices
	cl_event mapEvent;
	result.hostPtr = (pglNormalVertex*)clEnqueueMapBuffer(
		mGPUCommandQueue,
		result.memObject,
		CL_TRUE,
		CL_MAP_READ | CL_MAP_WRITE,
		0,
		memSize,
		0,
		NULL,
		&mapEvent,
		&mOCLError);
	if(mOCLError)
	{
		mError = PGL_OPENCL_ERROR;
		return result;
	}

	mOCLError = clWaitForEvents(1,&mapEvent);
	if(mOCLError)
		mError = PGL_OPENCL_ERROR;

	clReleaseEvent(mapEvent);

	return result;
}

void PGL::CLGL::unmapMemObject( pglMemObject pglm )
{
	// Unmap
	cl_event unmapEvent;
	mOCLError = clEnqueueUnmapMemObject(
		mGPUCommandQueue,
		pglm.memObject,
		pglm.hostPtr,
		0,
		NULL,
		NULL);

	if(mOCLError)
	{
		mError = PGL_OPENCL_ERROR;
	}
}

void PGL::CLGL::remapMemObject( pglMemObject pglm )
{
	// Remap new vertices
	cl_event mapEvent;
	pglm.hostPtr = (pglNormalVertex*)clEnqueueMapBuffer(
		mGPUCommandQueue,
		pglm.memObject,
		CL_TRUE,
		CL_MAP_READ | CL_MAP_WRITE,
		0,
		pglm.memSize,
		0,
		NULL,
		NULL,
		&mOCLError);
	if(mOCLError)
	{
		mError = PGL_OPENCL_ERROR;
		return;
	}

	if(mOCLError)
		mError = PGL_OPENCL_ERROR;
}

